#include <test/jtx.h>

#include <xrpl/protocol/Feature.h>

#include <algorithm>

namespace ripple {
namespace test {

// Helper function that returns the reserve on an account based on
// the passed in number of owners.
static XRPAmount
reserve(jtx::Env& env, std::uint32_t count)
{
    return env.current()->fees().accountReserve(count);
}

// Helper function that returns true if acct has the lsfDepostAuth flag set.
static bool
hasDepositAuth(jtx::Env const& env, jtx::Account const& acct)
{
    return ((*env.le(acct))[sfFlags] & lsfDepositAuth) == lsfDepositAuth;
}

struct DepositAuth_test : public beast::unit_test::suite
{
    void
    testEnable()
    {
        testcase("Enable");

        using namespace jtx;
        Account const alice{"alice"};

        {
            Env env(*this);
            env.fund(XRP(10000), alice);

            env(fset(alice, asfDepositAuth));
            env.close();
            BEAST_EXPECT(hasDepositAuth(env, alice));

            env(fclear(alice, asfDepositAuth));
            env.close();
            BEAST_EXPECT(!hasDepositAuth(env, alice));
        }
    }

    void
    testPayIOU()
    {
        // Exercise IOU payments and non-direct XRP payments to an account
        // that has the lsfDepositAuth flag set.
        testcase("Pay IOU");

        using namespace jtx;
        Account const alice{"alice"};
        Account const bob{"bob"};
        Account const carol{"carol"};
        Account const gw{"gw"};
        IOU const USD = gw["USD"];

        Env env(*this);

        env.fund(XRP(10000), alice, bob, carol, gw);
        env.close();
        env.trust(USD(1000), alice, bob);
        env.close();

        env(pay(gw, alice, USD(150)));
        env(offer(carol, USD(100), XRP(100)));
        env.close();

        // Make sure bob's trust line is all set up so he can receive USD.
        env(pay(alice, bob, USD(50)));
        env.close();

        // bob sets the lsfDepositAuth flag.
        env(fset(bob, asfDepositAuth), require(flags(bob, asfDepositAuth)));
        env.close();

        // None of the following payments should succeed.
        auto failedIouPayments = [this, &env, &alice, &bob, &USD]() {
            env.require(flags(bob, asfDepositAuth));

            // Capture bob's balances before hand to confirm they don't change.
            PrettyAmount const bobXrpBalance{env.balance(bob, XRP)};
            PrettyAmount const bobUsdBalance{env.balance(bob, USD)};

            env(pay(alice, bob, USD(50)), ter(tecNO_PERMISSION));
            env.close();

            // Note that even though alice is paying bob in XRP, the payment
            // is still not allowed since the payment passes through an offer.
            env(pay(alice, bob, drops(1)),
                sendmax(USD(1)),
                ter(tecNO_PERMISSION));
            env.close();

            BEAST_EXPECT(bobXrpBalance == env.balance(bob, XRP));
            BEAST_EXPECT(bobUsdBalance == env.balance(bob, USD));
        };

        //  Test when bob has an XRP balance > base reserve.
        failedIouPayments();

        // Set bob's XRP balance == base reserve.  Also demonstrate that
        // bob can make payments while his lsfDepositAuth flag is set.
        env(pay(bob, alice, USD(25)));
        env.close();

        {
            STAmount const bobPaysXRP{env.balance(bob, XRP) - reserve(env, 1)};
            XRPAmount const bobPaysFee{reserve(env, 1) - reserve(env, 0)};
            env(pay(bob, alice, bobPaysXRP), fee(bobPaysFee));
            env.close();
        }

        // Test when bob's XRP balance == base reserve.
        BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0));
        BEAST_EXPECT(env.balance(bob, USD) == USD(25));
        failedIouPayments();

        // Test when bob has an XRP balance == 0.
        env(noop(bob), fee(reserve(env, 0)));
        env.close();

        BEAST_EXPECT(env.balance(bob, XRP) == XRP(0));
        failedIouPayments();

        // Give bob enough XRP for the fee to clear the lsfDepositAuth flag.
        env(pay(alice, bob, drops(env.current()->fees().base)));

        // bob clears the lsfDepositAuth and the next payment succeeds.
        env(fclear(bob, asfDepositAuth));
        env.close();

        env(pay(alice, bob, USD(50)));
        env.close();

        env(pay(alice, bob, drops(1)), sendmax(USD(1)));
        env.close();
    }

    void
    testPayXRP()
    {
        // Exercise direct XRP payments to an account that has the
        // lsfDepositAuth flag set.
        testcase("Pay XRP");

        using namespace jtx;
        Account const alice{"alice"};
        Account const bob{"bob"};

        Env env(*this);
        auto const baseFee = env.current()->fees().base;

        env.fund(XRP(10000), alice, bob);
        env.close();

        // bob sets the lsfDepositAuth flag.
        env(fset(bob, asfDepositAuth), fee(drops(baseFee)));
        env.close();
        BEAST_EXPECT(env.balance(bob, XRP) == XRP(10000) - drops(baseFee));

        // bob has more XRP than the base reserve.  Any XRP payment should fail.
        env(pay(alice, bob, drops(1)), ter(tecNO_PERMISSION));
        env.close();
        BEAST_EXPECT(env.balance(bob, XRP) == XRP(10000) - drops(baseFee));

        // Change bob's XRP balance to exactly the base reserve.
        {
            STAmount const bobPaysXRP{env.balance(bob, XRP) - reserve(env, 1)};
            XRPAmount const bobPaysFee{reserve(env, 1) - reserve(env, 0)};
            env(pay(bob, alice, bobPaysXRP), fee(bobPaysFee));
            env.close();
        }

        // bob has exactly the base reserve.  A small enough direct XRP
        // payment should succeed.
        BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0));
        env(pay(alice, bob, drops(1)));
        env.close();

        // bob has exactly the base reserve + 1.  No payment should succeed.
        BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0) + drops(1));
        env(pay(alice, bob, drops(1)), ter(tecNO_PERMISSION));
        env.close();

        // Take bob down to a balance of 0 XRP.
        env(noop(bob), fee(reserve(env, 0) + drops(1)));
        env.close();
        BEAST_EXPECT(env.balance(bob, XRP) == drops(0));

        // We should not be able to pay bob more than the base reserve.
        env(pay(alice, bob, reserve(env, 0) + drops(1)), ter(tecNO_PERMISSION));
        env.close();

        // However a payment of exactly the base reserve should succeed.
        env(pay(alice, bob, reserve(env, 0) + drops(0)));
        env.close();
        BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0));

        // We should be able to pay bob the base reserve one more time.
        env(pay(alice, bob, reserve(env, 0) + drops(0)));
        env.close();
        BEAST_EXPECT(
            env.balance(bob, XRP) == (reserve(env, 0) + reserve(env, 0)));

        // bob's above the threshold again.  Any payment should fail.
        env(pay(alice, bob, drops(1)), ter(tecNO_PERMISSION));
        env.close();
        BEAST_EXPECT(
            env.balance(bob, XRP) == (reserve(env, 0) + reserve(env, 0)));

        // Take bob back down to a zero XRP balance.
        env(noop(bob), fee(env.balance(bob, XRP)));
        env.close();
        BEAST_EXPECT(env.balance(bob, XRP) == drops(0));

        // bob should not be able to clear lsfDepositAuth.
        env(fclear(bob, asfDepositAuth), ter(terINSUF_FEE_B));
        env.close();

        // We should be able to pay bob 1 drop now.
        env(pay(alice, bob, drops(1)));
        env.close();
        BEAST_EXPECT(env.balance(bob, XRP) == drops(1));

        // Pay bob enough so he can afford the fee to clear lsfDepositAuth.
        env(pay(alice, bob, drops(baseFee - 1)));
        env.close();

        // Interestingly, at this point the terINSUF_FEE_B retry grabs the
        // request to clear lsfDepositAuth.  So the balance should be zero
        // and lsfDepositAuth should be cleared.
        BEAST_EXPECT(env.balance(bob, XRP) == drops(0));
        env.require(nflags(bob, asfDepositAuth));

        // Since bob no longer has lsfDepositAuth set we should be able to
        // pay him more than the base reserve.
        env(pay(alice, bob, reserve(env, 0) + drops(1)));
        env.close();
        BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0) + drops(1));
    }

    void
    testNoRipple()
    {
        // It its current incarnation the DepositAuth flag does not change
        // any behaviors regarding rippling and the NoRipple flag.
        // Demonstrate that.
        testcase("No Ripple");

        using namespace jtx;
        Account const gw1("gw1");
        Account const gw2("gw2");
        Account const alice("alice");
        Account const bob("bob");

        IOU const USD1(gw1["USD"]);
        IOU const USD2(gw2["USD"]);

        auto testIssuer = [&](FeatureBitset const& features,
                              bool noRipplePrev,
                              bool noRippleNext,
                              bool withDepositAuth) {
            Env env(*this, features);

            env.fund(XRP(10000), gw1, alice, bob);
            env.close();
            env(trust(gw1, alice["USD"](10), noRipplePrev ? tfSetNoRipple : 0));
            env(trust(gw1, bob["USD"](10), noRippleNext ? tfSetNoRipple : 0));
            env.trust(USD1(10), alice, bob);

            env(pay(gw1, alice, USD1(10)));

            if (withDepositAuth)
                env(fset(gw1, asfDepositAuth));

            TER const result = (noRippleNext && noRipplePrev) ? TER{tecPATH_DRY}
                                                              : TER{tesSUCCESS};
            env(pay(alice, bob, USD1(10)), path(gw1), ter(result));
        };

        auto testNonIssuer = [&](FeatureBitset const& features,
                                 bool noRipplePrev,
                                 bool noRippleNext,
                                 bool withDepositAuth) {
            Env env(*this, features);

            env.fund(XRP(10000), gw1, gw2, alice);
            env.close();
            env(trust(alice, USD1(10), noRipplePrev ? tfSetNoRipple : 0));
            env(trust(alice, USD2(10), noRippleNext ? tfSetNoRipple : 0));
            env(pay(gw2, alice, USD2(10)));

            if (withDepositAuth)
                env(fset(alice, asfDepositAuth));

            TER const result = (noRippleNext && noRipplePrev) ? TER{tecPATH_DRY}
                                                              : TER{tesSUCCESS};
            env(pay(gw1, gw2, USD2(10)),
                path(alice),
                sendmax(USD1(10)),
                ter(result));
        };

        // Test every combo of noRipplePrev, noRippleNext, and withDepositAuth
        for (int i = 0; i < 8; ++i)
        {
            auto const noRipplePrev = i & 0x1;
            auto const noRippleNext = i & 0x2;
            auto const withDepositAuth = i & 0x4;
            testIssuer(
                testable_amendments(),
                noRipplePrev,
                noRippleNext,
                withDepositAuth);

            testNonIssuer(
                testable_amendments(),
                noRipplePrev,
                noRippleNext,
                withDepositAuth);
        }
    }

    void
    run() override
    {
        testEnable();
        testPayIOU();
        testPayXRP();
        testNoRipple();
    }
};

static Json::Value
ledgerEntryDepositPreauth(
    jtx::Env& env,
    jtx::Account const& acc,
    std::vector<jtx::deposit::AuthorizeCredentials> const& auth)
{
    Json::Value jvParams;
    jvParams[jss::ledger_index] = jss::validated;
    jvParams[jss::deposit_preauth][jss::owner] = acc.human();
    jvParams[jss::deposit_preauth][jss::authorized_credentials] =
        Json::arrayValue;
    auto& arr(jvParams[jss::deposit_preauth][jss::authorized_credentials]);
    for (auto const& o : auth)
    {
        arr.append(o.toLEJson());
    }
    return env.rpc("json", "ledger_entry", to_string(jvParams));
}

struct DepositPreauth_test : public beast::unit_test::suite
{
    void
    testEnable()
    {
        testcase("Enable");

        using namespace jtx;
        Account const alice{"alice"};
        Account const becky{"becky"};
        {
            //  o We should be able to add and remove an entry, and
            //  o That entry should cost one reserve.
            //  o The reserve should be returned when the entry is removed.
            Env env(*this);
            env.fund(XRP(10000), alice, becky);
            env.close();

            // Add a DepositPreauth to alice.
            env(deposit::auth(alice, becky));
            env.close();
            env.require(owners(alice, 1));
            env.require(owners(becky, 0));

            // Remove a DepositPreauth from alice.
            env(deposit::unauth(alice, becky));
            env.close();
            env.require(owners(alice, 0));
            env.require(owners(becky, 0));
        }
        {
            // Verify that an account can be preauthorized and unauthorized
            // using tickets.
            Env env(*this);
            env.fund(XRP(10000), alice, becky);
            env.close();

            env(ticket::create(alice, 2));
            std::uint32_t const aliceSeq{env.seq(alice)};
            env.close();
            env.require(tickets(alice, 2));

            // Consume the tickets from biggest seq to smallest 'cuz we can.
            std::uint32_t aliceTicketSeq{env.seq(alice)};

            // Add a DepositPreauth to alice.
            env(deposit::auth(alice, becky), ticket::use(--aliceTicketSeq));
            env.close();
            // Alice uses a ticket but gains a preauth entry.
            env.require(tickets(alice, 1));
            env.require(owners(alice, 2));
            BEAST_EXPECT(env.seq(alice) == aliceSeq);
            env.require(owners(becky, 0));

            // Remove a DepositPreauth from alice.
            env(deposit::unauth(alice, becky), ticket::use(--aliceTicketSeq));
            env.close();
            env.require(tickets(alice, 0));
            env.require(owners(alice, 0));
            BEAST_EXPECT(env.seq(alice) == aliceSeq);
            env.require(owners(becky, 0));
        }
    }

    void
    testInvalid()
    {
        testcase("Invalid");

        using namespace jtx;
        Account const alice{"alice"};
        Account const becky{"becky"};
        Account const carol{"carol"};

        Env env(*this);

        // Tell env about alice, becky and carol since they are not yet funded.
        env.memoize(alice);
        env.memoize(becky);
        env.memoize(carol);

        // Add DepositPreauth to an unfunded account.
        env(deposit::auth(alice, becky), seq(1), ter(terNO_ACCOUNT));

        env.fund(XRP(10000), alice, becky);
        env.close();

        // Bad fee.
        env(deposit::auth(alice, becky), fee(drops(-10)), ter(temBAD_FEE));
        env.close();

        // Bad flags.
        env(deposit::auth(alice, becky), txflags(tfSell), ter(temINVALID_FLAG));
        env.close();

        {
            // Neither auth not unauth.
            Json::Value tx{deposit::auth(alice, becky)};
            tx.removeMember(sfAuthorize.jsonName);
            env(tx, ter(temMALFORMED));
            env.close();
        }
        {
            // Both auth and unauth.
            Json::Value tx{deposit::auth(alice, becky)};
            tx[sfUnauthorize.jsonName] = becky.human();
            env(tx, ter(temMALFORMED));
            env.close();
        }
        {
            // Alice authorizes a zero account.
            Json::Value tx{deposit::auth(alice, becky)};
            tx[sfAuthorize.jsonName] = to_string(xrpAccount());
            env(tx, ter(temINVALID_ACCOUNT_ID));
            env.close();
        }

        // alice authorizes herself.
        env(deposit::auth(alice, alice), ter(temCANNOT_PREAUTH_SELF));
        env.close();

        // alice authorizes an unfunded account.
        env(deposit::auth(alice, carol), ter(tecNO_TARGET));
        env.close();

        // alice successfully authorizes becky.
        env.require(owners(alice, 0));
        env.require(owners(becky, 0));
        env(deposit::auth(alice, becky));
        env.close();
        env.require(owners(alice, 1));
        env.require(owners(becky, 0));

        // alice attempts to create a duplicate authorization.
        env(deposit::auth(alice, becky), ter(tecDUPLICATE));
        env.close();
        env.require(owners(alice, 1));
        env.require(owners(becky, 0));

        // carol attempts to preauthorize but doesn't have enough reserve.
        env.fund(drops(249'999'999), carol);
        env.close();

        env(deposit::auth(carol, becky), ter(tecINSUFFICIENT_RESERVE));
        env.close();
        env.require(owners(carol, 0));
        env.require(owners(becky, 0));

        // carol gets enough XRP to (barely) meet the reserve.
        env(pay(alice, carol, drops(env.current()->fees().base + 1)));
        env.close();
        env(deposit::auth(carol, becky));
        env.close();
        env.require(owners(carol, 1));
        env.require(owners(becky, 0));

        // But carol can't meet the reserve for another preauthorization.
        env(deposit::auth(carol, alice), ter(tecINSUFFICIENT_RESERVE));
        env.close();
        env.require(owners(carol, 1));
        env.require(owners(becky, 0));
        env.require(owners(alice, 1));

        // alice attempts to remove an authorization she doesn't have.
        env(deposit::unauth(alice, carol), ter(tecNO_ENTRY));
        env.close();
        env.require(owners(alice, 1));
        env.require(owners(becky, 0));

        // alice successfully removes her authorization of becky.
        env(deposit::unauth(alice, becky));
        env.close();
        env.require(owners(alice, 0));
        env.require(owners(becky, 0));

        // alice removes becky again and gets an error.
        env(deposit::unauth(alice, becky), ter(tecNO_ENTRY));
        env.close();
        env.require(owners(alice, 0));
        env.require(owners(becky, 0));
    }

    void
    testPayment(FeatureBitset features)
    {
        testcase("Payment");

        using namespace jtx;
        Account const alice{"alice"};
        Account const becky{"becky"};
        Account const gw{"gw"};
        IOU const USD(gw["USD"]);

        {
            // The initial implementation of DepositAuth had a bug where an
            // account with the DepositAuth flag set could not make a payment
            // to itself.  That bug was fixed in the DepositPreauth amendment.
            Env env(*this, features);
            env.fund(XRP(5000), alice, becky, gw);
            env.close();

            env.trust(USD(1000), alice);
            env.trust(USD(1000), becky);
            env.close();

            env(pay(gw, alice, USD(500)));
            env.close();

            env(offer(alice, XRP(100), USD(100), tfPassive),
                require(offers(alice, 1)));
            env.close();

            // becky pays herself USD (10) by consuming part of alice's offer.
            // Make sure the payment works if PaymentAuth is not involved.
            env(pay(becky, becky, USD(10)), path(~USD), sendmax(XRP(10)));
            env.close();

            // becky decides to require authorization for deposits.
            env(fset(becky, asfDepositAuth));
            env.close();

            // becky pays herself again.
            env(pay(becky, becky, USD(10)),
                path(~USD),
                sendmax(XRP(10)),
                ter(tesSUCCESS));
            env.close();

            {
                // becky setup depositpreauth with credentials
                char const credType[] = "abcde";
                Account const carol{"carol"};
                env.fund(XRP(5000), carol);
                env.close();

                bool const supportsCredentials = features[featureCredentials];

                TER const expectTer(
                    !supportsCredentials ? TER(temDISABLED) : TER(tesSUCCESS));

                env(deposit::authCredentials(becky, {{carol, credType}}),
                    ter(expectTer));
                env.close();

                // gw accept credentials
                env(credentials::create(gw, carol, credType), ter(expectTer));
                env.close();
                env(credentials::accept(gw, carol, credType), ter(expectTer));
                env.close();

                auto jv = credentials::ledgerEntry(env, gw, carol, credType);
                std::string const credIdx = supportsCredentials
                    ? jv[jss::result][jss::index].asString()
                    : "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6"
                      "EA288BE4";

                env(pay(gw, becky, USD(100)),
                    credentials::ids({credIdx}),
                    ter(expectTer));
                env.close();
            }
        }

        // Make sure DepositPreauthorization works for payments.

        Account const carol{"carol"};

        Env env(*this, features);
        env.fund(XRP(5000), alice, becky, carol, gw);
        env.close();

        env.trust(USD(1000), alice);
        env.trust(USD(1000), becky);
        env.trust(USD(1000), carol);
        env.close();

        env(pay(gw, alice, USD(1000)));
        env.close();

        // Make XRP and IOU payments from alice to becky.  Should be fine.
        env(pay(alice, becky, XRP(100)));
        env(pay(alice, becky, USD(100)));
        env.close();

        // becky decides to require authorization for deposits.
        env(fset(becky, asfDepositAuth));
        env.close();

        // alice can no longer pay becky.
        env(pay(alice, becky, XRP(100)), ter(tecNO_PERMISSION));
        env(pay(alice, becky, USD(100)), ter(tecNO_PERMISSION));
        env.close();

        // becky preauthorizes carol for deposit, which doesn't provide
        // authorization for alice.
        env(deposit::auth(becky, carol));
        env.close();

        // alice still can't pay becky.
        env(pay(alice, becky, XRP(100)), ter(tecNO_PERMISSION));
        env(pay(alice, becky, USD(100)), ter(tecNO_PERMISSION));
        env.close();

        // becky preauthorizes alice for deposit.
        env(deposit::auth(becky, alice));
        env.close();

        // alice can now pay becky.
        env(pay(alice, becky, XRP(100)));
        env(pay(alice, becky, USD(100)));
        env.close();

        // alice decides to require authorization for deposits.
        env(fset(alice, asfDepositAuth));
        env.close();

        // Even though alice is authorized to pay becky, becky is not
        // authorized to pay alice.
        env(pay(becky, alice, XRP(100)), ter(tecNO_PERMISSION));
        env(pay(becky, alice, USD(100)), ter(tecNO_PERMISSION));
        env.close();

        // becky unauthorizes carol.  Should have no impact on alice.
        env(deposit::unauth(becky, carol));
        env.close();

        env(pay(alice, becky, XRP(100)));
        env(pay(alice, becky, USD(100)));
        env.close();

        // becky unauthorizes alice.  alice now can't pay becky.
        env(deposit::unauth(becky, alice));
        env.close();

        env(pay(alice, becky, XRP(100)), ter(tecNO_PERMISSION));
        env(pay(alice, becky, USD(100)), ter(tecNO_PERMISSION));
        env.close();

        // becky decides to remove authorization for deposits.  Now
        // alice can pay becky again.
        env(fclear(becky, asfDepositAuth));
        env.close();

        env(pay(alice, becky, XRP(100)));
        env(pay(alice, becky, USD(100)));
        env.close();
    }

    void
    testCredentialsPayment()
    {
        using namespace jtx;

        char const credType[] = "abcde";
        Account const issuer{"issuer"};
        Account const alice{"alice"};
        Account const bob{"bob"};
        Account const maria{"maria"};
        Account const john{"john"};

        {
            testcase("Payment failure with disabled credentials rule.");

            Env env(*this, testable_amendments() - featureCredentials);

            env.fund(XRP(5000), issuer, bob, alice);
            env.close();

            // Bob require preauthorization
            env(fset(bob, asfDepositAuth));
            env.close();

            // Setup DepositPreauth object failed - amendent is not supported
            env(deposit::authCredentials(bob, {{issuer, credType}}),
                ter(temDISABLED));
            env.close();

            // But can create old DepositPreauth
            env(deposit::auth(bob, alice));
            env.close();

            // And alice can't pay with any credentials, amendement is not
            // enabled
            std::string const invalidIdx =
                "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E"
                "01E034";
            env(pay(alice, bob, XRP(10)),
                credentials::ids({invalidIdx}),
                ter(temDISABLED));
            env.close();
        }

        {
            testcase("Payment with credentials.");

            Env env(*this);

            env.fund(XRP(5000), issuer, alice, bob, john);
            env.close();

            // Issuer create credentials, but Alice didn't accept them yet
            env(credentials::create(alice, issuer, credType));
            env.close();

            // Get the index of the credentials
            auto const jv =
                credentials::ledgerEntry(env, alice, issuer, credType);
            std::string const credIdx = jv[jss::result][jss::index].asString();

            // Bob require preauthorization
            env(fset(bob, asfDepositAuth));
            env.close();

            // Bob will accept payements from accounts with credentials signed
            // by 'issuer'
            env(deposit::authCredentials(bob, {{issuer, credType}}));
            env.close();

            auto const jDP =
                ledgerEntryDepositPreauth(env, bob, {{issuer, credType}});
            BEAST_EXPECT(
                jDP.isObject() && jDP.isMember(jss::result) &&
                !jDP[jss::result].isMember(jss::error) &&
                jDP[jss::result].isMember(jss::node) &&
                jDP[jss::result][jss::node].isMember("LedgerEntryType") &&
                jDP[jss::result][jss::node]["LedgerEntryType"] ==
                    jss::DepositPreauth);

            // Alice can't pay - empty credentials array
            {
                auto jv = pay(alice, bob, XRP(100));
                jv[sfCredentialIDs.jsonName] = Json::arrayValue;
                env(jv, ter(temMALFORMED));
                env.close();
            }

            // Alice can't pay - not accepted credentials
            env(pay(alice, bob, XRP(100)),
                credentials::ids({credIdx}),
                ter(tecBAD_CREDENTIALS));
            env.close();

            // Alice accept the credentials
            env(credentials::accept(alice, issuer, credType));
            env.close();

            // Now Alice can pay
            env(pay(alice, bob, XRP(100)), credentials::ids({credIdx}));
            env.close();

            // Alice can pay Maria without depositPreauth enabled
            env(pay(alice, maria, XRP(250)), credentials::ids({credIdx}));
            env.close();

            // john can accept payment with old depositPreauth and valid
            // credentials
            env(fset(john, asfDepositAuth));
            env(deposit::auth(john, alice));
            env(pay(alice, john, XRP(100)), credentials::ids({credIdx}));
            env.close();
        }

        {
            testcase("Payment failure with invalid credentials.");

            Env env(*this);

            env.fund(XRP(10000), issuer, alice, bob, maria);
            env.close();

            // Issuer create credentials, but Alice didn't accept them yet
            env(credentials::create(alice, issuer, credType));
            env.close();
            // Alice accept the credentials
            env(credentials::accept(alice, issuer, credType));
            env.close();
            // Get the index of the credentials
            auto const jv =
                credentials::ledgerEntry(env, alice, issuer, credType);
            std::string const credIdx = jv[jss::result][jss::index].asString();

            {
                // Success as destination didn't enable preauthorization so
                // valid credentials will not fail
                env(pay(alice, bob, XRP(100)), credentials::ids({credIdx}));
            }

            // Bob require preauthorization
            env(fset(bob, asfDepositAuth));
            env.close();

            {
                // Fail as destination didn't setup DepositPreauth object
                env(pay(alice, bob, XRP(100)),
                    credentials::ids({credIdx}),
                    ter(tecNO_PERMISSION));
            }

            // Bob setup DepositPreauth object, duplicates is not allowed
            env(deposit::authCredentials(
                    bob, {{issuer, credType}, {issuer, credType}}),
                ter(temMALFORMED));

            // Bob setup DepositPreauth object
            env(deposit::authCredentials(bob, {{issuer, credType}}));
            env.close();

            {
                std::string const invalidIdx =
                    "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E"
                    "01E034";
                // Alice can't pay with non-existing credentials
                env(pay(alice, bob, XRP(100)),
                    credentials::ids({invalidIdx}),
                    ter(tecBAD_CREDENTIALS));
            }

            {  // maria can't pay using valid credentials but issued for
               // different account
                env(pay(maria, bob, XRP(100)),
                    credentials::ids({credIdx}),
                    ter(tecBAD_CREDENTIALS));
            }

            {
                // create another valid credential
                char const credType2[] = "fghij";
                env(credentials::create(alice, issuer, credType2));
                env.close();
                env(credentials::accept(alice, issuer, credType2));
                env.close();
                auto const jv =
                    credentials::ledgerEntry(env, alice, issuer, credType2);
                std::string const credIdx2 =
                    jv[jss::result][jss::index].asString();

                // Alice can't pay with invalid set of valid credentials
                env(pay(alice, bob, XRP(100)),
                    credentials::ids({credIdx, credIdx2}),
                    ter(tecNO_PERMISSION));
            }

            // Error, duplicate credentials
            env(pay(alice, bob, XRP(100)),
                credentials::ids({credIdx, credIdx}),
                ter(temMALFORMED));

            // Alice can pay
            env(pay(alice, bob, XRP(100)), credentials::ids({credIdx}));
            env.close();
        }
    }

    void
    testCredentialsCreation()
    {
        using namespace jtx;

        char const credType[] = "abcde";
        Account const issuer{"issuer"};
        Account const alice{"alice"};
        Account const bob{"bob"};
        Account const maria{"maria"};

        {
            testcase("Creating / deleting with credentials.");

            Env env(*this);

            env.fund(XRP(5000), issuer, alice, bob);
            env.close();

            {
                // both included [AuthorizeCredentials UnauthorizeCredentials]
                auto jv = deposit::authCredentials(bob, {{issuer, credType}});
                jv[sfUnauthorizeCredentials.jsonName] = Json::arrayValue;
                env(jv, ter(temMALFORMED));
            }

            {
                // both included [Unauthorize, AuthorizeCredentials]
                auto jv = deposit::authCredentials(bob, {{issuer, credType}});
                jv[sfUnauthorize.jsonName] = issuer.human();
                env(jv, ter(temMALFORMED));
            }

            {
                // both included [Authorize, AuthorizeCredentials]
                auto jv = deposit::authCredentials(bob, {{issuer, credType}});
                jv[sfAuthorize.jsonName] = issuer.human();
                env(jv, ter(temMALFORMED));
            }

            {
                // both included [Unauthorize, UnauthorizeCredentials]
                auto jv = deposit::unauthCredentials(bob, {{issuer, credType}});
                jv[sfUnauthorize.jsonName] = issuer.human();
                env(jv, ter(temMALFORMED));
            }

            {
                // both included [Authorize, UnauthorizeCredentials]
                auto jv = deposit::unauthCredentials(bob, {{issuer, credType}});
                jv[sfAuthorize.jsonName] = issuer.human();
                env(jv, ter(temMALFORMED));
            }

            {
                // AuthorizeCredentials is empty
                auto jv = deposit::authCredentials(bob, {});
                env(jv, ter(temARRAY_EMPTY));
            }

            {
                // invalid issuer
                auto jv = deposit::authCredentials(bob, {});
                auto& arr(jv[sfAuthorizeCredentials.jsonName]);
                Json::Value cred = Json::objectValue;
                cred[jss::Issuer] = to_string(xrpAccount());
                cred[sfCredentialType.jsonName] =
                    strHex(std::string_view(credType));
                Json::Value credParent;
                credParent[jss::Credential] = cred;
                arr.append(std::move(credParent));

                env(jv, ter(temINVALID_ACCOUNT_ID));
            }

            {
                // empty credential type
                auto jv = deposit::authCredentials(bob, {{issuer, {}}});
                env(jv, ter(temMALFORMED));
            }

            {
                // AuthorizeCredentials is larger than 8 elements
                Account const a("a"), b("b"), c("c"), d("d"), e("e"), f("f"),
                    g("g"), h("h"), i("i");
                auto const& z = credType;
                auto jv = deposit::authCredentials(
                    bob,
                    {{a, z},
                     {b, z},
                     {c, z},
                     {d, z},
                     {e, z},
                     {f, z},
                     {g, z},
                     {h, z},
                     {i, z}});
                env(jv, ter(temARRAY_TOO_LARGE));
            }

            {
                // Can't create with non-existing issuer
                Account const rick{"rick"};
                auto jv = deposit::authCredentials(bob, {{rick, credType}});
                env(jv, ter(tecNO_ISSUER));
                env.close();
            }

            {
                // not enough reserve
                Account const john{"john"};
                env.fund(env.current()->fees().accountReserve(0), john);
                env.close();
                auto jv = deposit::authCredentials(john, {{issuer, credType}});
                env(jv, ter(tecINSUFFICIENT_RESERVE));
            }

            {
                // NO deposit object exists
                env(deposit::unauthCredentials(bob, {{issuer, credType}}),
                    ter(tecNO_ENTRY));
            }

            // Create DepositPreauth object
            {
                env(deposit::authCredentials(bob, {{issuer, credType}}));
                env.close();

                auto const jDP =
                    ledgerEntryDepositPreauth(env, bob, {{issuer, credType}});
                BEAST_EXPECT(
                    jDP.isObject() && jDP.isMember(jss::result) &&
                    !jDP[jss::result].isMember(jss::error) &&
                    jDP[jss::result].isMember(jss::node) &&
                    jDP[jss::result][jss::node].isMember("LedgerEntryType") &&
                    jDP[jss::result][jss::node]["LedgerEntryType"] ==
                        jss::DepositPreauth);

                // Check object fields
                BEAST_EXPECT(
                    jDP[jss::result][jss::node][jss::Account] == bob.human());
                auto const& credentials(
                    jDP[jss::result][jss::node]["AuthorizeCredentials"]);
                BEAST_EXPECT(credentials.isArray() && credentials.size() == 1);
                for (auto const& o : credentials)
                {
                    auto const& c(o[jss::Credential]);
                    BEAST_EXPECT(c[jss::Issuer].asString() == issuer.human());
                    BEAST_EXPECT(
                        c["CredentialType"].asString() ==
                        strHex(std::string_view(credType)));
                }

                // can't create duplicate
                env(deposit::authCredentials(bob, {{issuer, credType}}),
                    ter(tecDUPLICATE));
            }

            // Delete DepositPreauth object
            {
                env(deposit::unauthCredentials(bob, {{issuer, credType}}));
                env.close();
                auto const jDP =
                    ledgerEntryDepositPreauth(env, bob, {{issuer, credType}});
                BEAST_EXPECT(
                    jDP.isObject() && jDP.isMember(jss::result) &&
                    jDP[jss::result].isMember(jss::error) &&
                    jDP[jss::result][jss::error] == "entryNotFound");
            }
        }
    }

    void
    testExpiredCreds()
    {
        using namespace jtx;
        char const credType[] = "abcde";
        char const credType2[] = "fghijkl";
        Account const issuer{"issuer"};
        Account const alice{"alice"};
        Account const bob{"bob"};
        Account const gw{"gw"};
        IOU const USD = gw["USD"];
        Account const zelda{"zelda"};

        {
            testcase("Payment failure with expired credentials.");

            Env env(*this);

            env.fund(XRP(10000), issuer, alice, bob, gw);
            env.close();

            // Create credentials
            auto jv = credentials::create(alice, issuer, credType);
            // Current time in ripple epoch.
            // Every time ledger close, unittest timer increase by 10s
            uint32_t const t = env.current()
                                   ->info()
                                   .parentCloseTime.time_since_epoch()
                                   .count() +
                60;
            jv[sfExpiration.jsonName] = t;
            env(jv);
            env.close();

            // Alice accept the credentials
            env(credentials::accept(alice, issuer, credType));
            env.close();

            // Create credential which not expired
            jv = credentials::create(alice, issuer, credType2);
            uint32_t const t2 = env.current()
                                    ->info()
                                    .parentCloseTime.time_since_epoch()
                                    .count() +
                1000;
            jv[sfExpiration.jsonName] = t2;
            env(jv);
            env.close();
            env(credentials::accept(alice, issuer, credType2));
            env.close();

            BEAST_EXPECT(ownerCount(env, issuer) == 0);
            BEAST_EXPECT(ownerCount(env, alice) == 2);

            // Get the index of the credentials
            jv = credentials::ledgerEntry(env, alice, issuer, credType);
            std::string const credIdx = jv[jss::result][jss::index].asString();
            jv = credentials::ledgerEntry(env, alice, issuer, credType2);
            std::string const credIdx2 = jv[jss::result][jss::index].asString();

            // Bob require preauthorization
            env(fset(bob, asfDepositAuth));
            env.close();
            // Bob setup DepositPreauth object
            env(deposit::authCredentials(
                bob, {{issuer, credType}, {issuer, credType2}}));
            env.close();

            {
                // Alice can pay
                env(pay(alice, bob, XRP(100)),
                    credentials::ids({credIdx, credIdx2}));
                env.close();
                env.close();

                // Ledger closed, time increased, alice can't pay anymore
                env(pay(alice, bob, XRP(100)),
                    credentials::ids({credIdx, credIdx2}),
                    ter(tecEXPIRED));
                env.close();

                {
                    // check that expired credentials were deleted
                    auto const jDelCred =
                        credentials::ledgerEntry(env, alice, issuer, credType);
                    BEAST_EXPECT(
                        jDelCred.isObject() && jDelCred.isMember(jss::result) &&
                        jDelCred[jss::result].isMember(jss::error) &&
                        jDelCred[jss::result][jss::error] == "entryNotFound");
                }

                {
                    // check that non-expired credential still present
                    auto const jle =
                        credentials::ledgerEntry(env, alice, issuer, credType2);
                    BEAST_EXPECT(
                        jle.isObject() && jle.isMember(jss::result) &&
                        !jle[jss::result].isMember(jss::error) &&
                        jle[jss::result].isMember(jss::node) &&
                        jle[jss::result][jss::node].isMember(
                            "LedgerEntryType") &&
                        jle[jss::result][jss::node]["LedgerEntryType"] ==
                            jss::Credential &&
                        jle[jss::result][jss::node][jss::Issuer] ==
                            issuer.human() &&
                        jle[jss::result][jss::node][jss::Subject] ==
                            alice.human() &&
                        jle[jss::result][jss::node]["CredentialType"] ==
                            strHex(std::string_view(credType2)));
                }

                BEAST_EXPECT(ownerCount(env, issuer) == 0);
                BEAST_EXPECT(ownerCount(env, alice) == 1);
            }

            {
                auto jv = credentials::create(gw, issuer, credType);
                uint32_t const t = env.current()
                                       ->info()
                                       .parentCloseTime.time_since_epoch()
                                       .count() +
                    40;
                jv[sfExpiration.jsonName] = t;
                env(jv);
                env.close();
                env(credentials::accept(gw, issuer, credType));
                env.close();

                jv = credentials::ledgerEntry(env, gw, issuer, credType);
                std::string const credIdx =
                    jv[jss::result][jss::index].asString();

                BEAST_EXPECT(ownerCount(env, issuer) == 0);
                BEAST_EXPECT(ownerCount(env, gw) == 1);

                env.close();
                env.close();
                env.close();

                // credentials are expired
                env(pay(gw, bob, USD(150)),
                    credentials::ids({credIdx}),
                    ter(tecEXPIRED));
                env.close();

                // check that expired credentials were deleted
                auto const jDelCred =
                    credentials::ledgerEntry(env, gw, issuer, credType);
                BEAST_EXPECT(
                    jDelCred.isObject() && jDelCred.isMember(jss::result) &&
                    jDelCred[jss::result].isMember(jss::error) &&
                    jDelCred[jss::result][jss::error] == "entryNotFound");

                BEAST_EXPECT(ownerCount(env, issuer) == 0);
                BEAST_EXPECT(ownerCount(env, gw) == 0);
            }
        }

        {
            using namespace std::chrono;

            testcase("Escrow failure with expired credentials.");

            Env env(*this);

            env.fund(XRP(5000), issuer, alice, bob, zelda);
            env.close();

            // Create credentials
            auto jv = credentials::create(zelda, issuer, credType);
            uint32_t const t = env.current()
                                   ->info()
                                   .parentCloseTime.time_since_epoch()
                                   .count() +
                50;
            jv[sfExpiration.jsonName] = t;
            env(jv);
            env.close();

            // Zelda accept the credentials
            env(credentials::accept(zelda, issuer, credType));
            env.close();

            // Get the index of the credentials
            jv = credentials::ledgerEntry(env, zelda, issuer, credType);
            std::string const credIdx = jv[jss::result][jss::index].asString();

            // Bob require preauthorization
            env(fset(bob, asfDepositAuth));
            env.close();
            // Bob setup DepositPreauth object
            env(deposit::authCredentials(bob, {{issuer, credType}}));
            env.close();

            auto const seq = env.seq(alice);
            env(escrow::create(alice, bob, XRP(1000)),
                escrow::finish_time(env.now() + 1s));
            env.close();

            // zelda can't finish escrow with invalid credentials
            {
                env(escrow::finish(zelda, alice, seq),
                    credentials::ids({}),
                    ter(temMALFORMED));
                env.close();
            }

            {
                // zelda can't finish escrow with invalid credentials
                std::string const invalidIdx =
                    "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E"
                    "01E034";

                env(escrow::finish(zelda, alice, seq),
                    credentials::ids({invalidIdx}),
                    ter(tecBAD_CREDENTIALS));
                env.close();
            }

            {  // Ledger closed, time increased, zelda can't finish escrow
                env(escrow::finish(zelda, alice, seq),
                    credentials::ids({credIdx}),
                    fee(1500),
                    ter(tecEXPIRED));
                env.close();
            }

            // check that expired credentials were deleted
            auto const jDelCred =
                credentials::ledgerEntry(env, zelda, issuer, credType);
            BEAST_EXPECT(
                jDelCred.isObject() && jDelCred.isMember(jss::result) &&
                jDelCred[jss::result].isMember(jss::error) &&
                jDelCred[jss::result][jss::error] == "entryNotFound");
        }
    }

    void
    testSortingCredentials()
    {
        using namespace jtx;

        Account const stock{"stock"};
        Account const alice{"alice"};
        Account const bob{"bob"};

        Env env(*this);

        testcase("Sorting credentials.");

        env.fund(XRP(5000), stock, alice, bob);

        std::vector<deposit::AuthorizeCredentials> credentials = {
            {"a", "a"},
            {"b", "b"},
            {"c", "c"},
            {"d", "d"},
            {"e", "e"},
            {"f", "f"},
            {"g", "g"},
            {"h", "h"}};

        for (auto const& c : credentials)
            env.fund(XRP(5000), c.issuer);
        env.close();

        std::random_device rd;
        std::mt19937 gen(rd());

        {
            std::unordered_map<std::string, Account> pubKey2Acc;
            for (auto const& c : credentials)
                pubKey2Acc.emplace(c.issuer.human(), c.issuer);

            // check sorting in object
            for (int i = 0; i < 10; ++i)
            {
                std::ranges::shuffle(credentials, gen);
                env(deposit::authCredentials(stock, credentials));
                env.close();

                auto const dp =
                    ledgerEntryDepositPreauth(env, stock, credentials);
                auto const& authCred(
                    dp[jss::result][jss::node]["AuthorizeCredentials"]);
                BEAST_EXPECT(
                    authCred.isArray() &&
                    authCred.size() == credentials.size());
                std::vector<std::pair<Account, std::string>> readedCreds;
                for (auto const& o : authCred)
                {
                    auto const& c(o[jss::Credential]);
                    auto issuer = c[jss::Issuer].asString();

                    if (BEAST_EXPECT(pubKey2Acc.contains(issuer)))
                        readedCreds.emplace_back(
                            pubKey2Acc.at(issuer),
                            c["CredentialType"].asString());
                }

                BEAST_EXPECT(std::ranges::is_sorted(readedCreds));

                env(deposit::unauthCredentials(stock, credentials));
                env.close();
            }
        }

        {
            std::ranges::shuffle(credentials, gen);
            env(deposit::authCredentials(stock, credentials));
            env.close();

            // check sorting in params
            for (int i = 0; i < 10; ++i)
            {
                std::ranges::shuffle(credentials, gen);
                env(deposit::authCredentials(stock, credentials),
                    ter(tecDUPLICATE));
            }
        }

        testcase("Check duplicate credentials.");
        {
            // check duplicates in depositPreauth params
            std::vector<deposit::AuthorizeCredentials> copyCredentials(
                credentials.begin(), credentials.end() - 1);

            std::ranges::shuffle(copyCredentials, gen);
            for (auto const& c : copyCredentials)
            {
                auto credentials2 = copyCredentials;
                credentials2.push_back(c);
                env(deposit::authCredentials(stock, credentials2),
                    ter(temMALFORMED));
            }

            // create batch of credentials and save their hashes
            std::vector<std::string> credentialIDs;
            for (auto const& c : credentials)
            {
                env(credentials::create(alice, c.issuer, c.credType));
                env.close();
                env(credentials::accept(alice, c.issuer, c.credType));
                env.close();

                credentialIDs.push_back(credentials::ledgerEntry(
                                            env,
                                            alice,
                                            c.issuer,
                                            c.credType)[jss::result][jss::index]
                                            .asString());
            }

            // check duplicates in payment params
            for (auto const& h : credentialIDs)
            {
                auto credentialIDs2 = credentialIDs;
                credentialIDs2.push_back(h);

                env(pay(alice, bob, XRP(100)),
                    credentials::ids(credentialIDs2),
                    ter(temMALFORMED));
            }
        }
    }

    void
    run() override
    {
        testEnable();
        testInvalid();
        auto const supported{jtx::testable_amendments()};
        testPayment(supported - featureCredentials);
        testPayment(supported);
        testCredentialsPayment();
        testCredentialsCreation();
        testExpiredCreds();
        testSortingCredentials();
    }
};

BEAST_DEFINE_TESTSUITE(DepositAuth, app, ripple);
BEAST_DEFINE_TESTSUITE(DepositPreauth, app, ripple);

}  // namespace test
}  // namespace ripple
